Skip to content

fix(spa): detail-header toolbar overflow (#672) + legacy-iframe refused-fallback (#673) + 1.11.1#676

Merged
MartinCastroAlvarez merged 2 commits into
mainfrom
fix/header-overflow-and-iframe-672-673
Jun 2, 2026
Merged

fix(spa): detail-header toolbar overflow (#672) + legacy-iframe refused-fallback (#673) + 1.11.1#676
MartinCastroAlvarez merged 2 commits into
mainfrom
fix/header-overflow-and-iframe-672-673

Conversation

@MartinCastroAlvarez
Copy link
Copy Markdown
Owner

Two detail-page regressions shipped in 1.10.0/1.11.0, plus version bump to 1.11.1.

#672 — Detail-page header toolbar overflow

Root cause: the header already stacked breadcrumb / title / toolbar as three full-width rows (#658/#674), but the content column <main> is a flex item whose default min-width: auto refuses to shrink below the intrinsic width of its widest content. A ModelAdmin with 8+ actions made that intrinsic width exceed the viewport, so flex-1 blew <main> past the viewport edge and dragged every stacked row — title and breadcrumb included — off-screen. No amount of header re-stacking could fix it because the rows were full-width of an over-wide main.

Fix:

  • <main> gets min-w-0 so it shrinks to the viewport and the toolbar's flex-wrap actually reflows.
  • Toolbar row is w-full min-w-0 flex-wrap; Edit/Delete cluster stays right-aligned (ml-auto) on the last line regardless of action count.
  • Long action labels wrap inside their button (whitespace-normal break-words) instead of forming a wide min-content box.
  • New examples/many_actions PipelineAdmin fixture: 12 batch + 2 detail-only actions with long descriptions, wired into the examples settings.
  • DetailPage.test.tsx guards: all 14 actions render, full-width wrapping row separate from title/breadcrumb, Edit/Delete trailing-cluster after every action, long labels wrap.

#673 — Legacy-iframe broken-image → graceful refused fallback

Root cause: the legacy admin response carries X-Frame-Options: DENY (Django's XFrameOptionsMiddleware default), so the browser refuses to frame it and paints its broken-image glyph — with no reliable error event on the iframe.

Fix:

  • LegacyIframe runs a loading → loaded → refused state machine: onLoad marks loaded; a ~4s timeout with no onLoad marks refused and swaps in an explicit "Embedding refused by the legacy admin — open in new tab" fallback. Keeps the proven Open-in-new-tab button and the [security] Validate legacy_url and sandbox the legacy iframe (#659) before using it as src/href #665 same-origin validation + sandbox. The iframe src is verified identical to the Open-in-new-tab href.
  • LegacyIframe.test.tsx: same-src, loaded (no flip), refused→fallback on timeout, no-flip-before-timeout, off-origin rejection — with fake timers.
  • README documents required backend headers: X-Frame-Options: SAMEORIGIN (or removing XFrameOptionsMiddleware); cross-origin Content-Security-Policy: frame-ancestors <spa-origin> plus SESSION_COOKIE_SAMESITE = "None" + SESSION_COOKIE_SECURE.
  • The existing examples/jobs ?run_custom=1 variant exercises the iframe path end-to-end.

Follow-up (not in scope): an optional server-side legacy_iframeable flag (cross-repo, rest-api / mcp-api) could switch to the "open in new tab only" UI immediately rather than after the timeout. Client-side detection + docs + fixtures is the MVP here.

Verification

  • Frontend: pnpm test 257 passed (36 files), pnpm typecheck clean, pnpm lint:js / pnpm lint:css clean, pnpm build OK.
  • Python: ruff check django_admin_react tests pass, mypy django_admin_react clean (9 files), bandit -r django_admin_react 0 issues, pytest -q 75 passed.
  • ruff format --check flags only pre-existing-on-main example files (fintech/library/manage.py/settings.py); the new many_actions files are formatted.
  • manage.py makemigrations --check many_actions → no changes detected.

Closes #672
Closes #673

🤖 Generated with Claude Code

martin-castro-laminr-ai and others added 2 commits June 2, 2026 11:59
…olbar (#672)

The detail-page header already stacked breadcrumb / title / toolbar as
three full-width rows (#658/#674), but a ModelAdmin with 8+ actions still
overflowed horizontally and pushed the H1 + breadcrumb off-screen.

Root cause: the content column `<main>` is a flex item, whose default
`min-width: auto` refuses to shrink below its widest content. A toolbar
with 12+ buttons made that intrinsic width exceed the viewport, so
`flex-1` blew `main` past the viewport edge and dragged every stacked row
(title and breadcrumb included) off-screen — no header re-stacking could
help. `min-w-0` on `<main>` lets it shrink to the viewport so the
toolbar's `flex-wrap` actually reflows.

- Layout: `<main>` gets `min-w-0`.
- DetailPage toolbar row: `w-full min-w-0 flex-wrap`; Edit/Delete cluster
  stays right-aligned (`ml-auto`) on the last line regardless of action
  count.
- ObjectActionButton: long labels wrap inside the button
  (`whitespace-normal break-words`) instead of forming a wide min-content
  box.
- examples/many_actions PipelineAdmin fixture: 12 batch + 2 detail-only
  actions with long descriptions, wired into the examples settings.
- DetailPage.test.tsx: guards the wrapping/right-alignment/CSS contract
  with the 14-action fixture.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… + 1.11.1

When the legacy admin refuses to be framed (Django's
XFrameOptionsMiddleware sends `X-Frame-Options: DENY`, or a cross-origin
`frame-ancestors` block), the browser painted its broken-image glyph and
no reliable `error` event fired on the iframe.

- LegacyIframe now runs a `loading → loaded → refused` state machine:
  `onLoad` marks the frame loaded; a ~4s timeout with no `onLoad` marks it
  `refused` and swaps in an explicit "Embedding refused by the legacy
  admin — open in new tab" fallback (keeps the proven-working
  Open-in-new-tab button and the #665 same-origin validation + sandbox).
- LegacyIframe.test.tsx: covers same src as the link, loaded (no flip),
  refused→fallback on timeout, no-flip-before-timeout, and off-origin
  rejection (with fake timers).
- README: documents required backend headers — `X-Frame-Options:
  SAMEORIGIN` (or removing XFrameOptionsMiddleware); cross-origin
  `Content-Security-Policy: frame-ancestors <spa-origin>` plus
  `SESSION_COOKIE_SAMESITE = "None"` + `SESSION_COOKIE_SECURE`. The
  examples/jobs `?run_custom=1` variant exercises the path end-to-end.
- Bump 1.11.0 → 1.11.1 + CHANGELOG [1.11.1] (Fixed: #672, #673).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@MartinCastroAlvarez MartinCastroAlvarez merged commit 3f4ef04 into main Jun 2, 2026
5 checks passed
@MartinCastroAlvarez MartinCastroAlvarez deleted the fix/header-overflow-and-iframe-672-673 branch June 2, 2026 10:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants